import java.awt.Color;
import java.awt.Graphics;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Queue;


public abstract class Simulation implements Runnable {

	public int SCALAR_X = 5;
	public final int SPEED = 25;
	public final int TEXT_SHIFT = 3;
	public final int SHIFT_X = (int)(50 / SCALAR_X); //always starts in the same area
	private boolean completed;
	
	private int num_processes;
	@SuppressWarnings("unused")
	private int times_processes;
	ProcessNode current_process;
	private boolean newSimulationRequested;
	
	public ArrayList<ProcessNode> processList;
	Queue<ProcessNode> processQueue;
	public ArrayList<DrawNode> drawNodes;
	
	private StringBuffer feedback;
	public int timer;
	
	public abstract boolean interuptProcessSwitchConditions();
	
	public Simulation(int num_processes, int times_processes, Queue<ProcessNode> queue) {
		this.current_process = null;
		this.num_processes = num_processes;
		this.times_processes = times_processes;
		this.drawNodes = new ArrayList<DrawNode>();
		this.timer = 0;
		this.feedback = new StringBuffer();
		this.newSimulationRequested = false;
		this.completed = false;
		
		this.processList = new ArrayList<ProcessNode>();
		this.processQueue = queue;
		
		//Generate Processes
		for(int k = 0; k < times_processes; k++)
			for(int i = 0; i < num_processes; i++)
			{
				//Create Process 
				String process_name = "P" + (i+1);
				ProcessNode p_node = new ProcessNode(process_name, this);
				processList.add(p_node);
			}
		processList.get(0).setArrivalTime(0);
		Collections.sort(processList, new Comparator<ProcessNode>() { 
			public int compare(ProcessNode a, ProcessNode b){
				if(a.getArrivalTime() < b.getArrivalTime()) return -1;
				else if (a.getArrivalTime() > b.getArrivalTime()) return 1;
				else return 0;
			}});
		feedback.append("\n"); //Newline
		for(ProcessNode pn : processList) {
			feedback.append("Process: " + pn.getProcessName() + " arrives at " + pn.getArrivalTime() + " with runtime " + pn.getServiceTime() +   "\n");
		}
	}
	
	public void run()
	{
		while(	!(noFutureProcesses() && noAvailableProcess() && noCurrentProcess()) ) {
		
			if(newSimulationRequested) 
				break;
			
			addNewlyArrivingProcessesToQueue();
			
			if(noCurrentProcess()) {
				if(!noAvailableProcess())
					switchProcess();
			} else {
				//if there is a current process check if it should be switched
				if(currentProcessFinished() || interuptProcessSwitchConditions()){
					if(!currentProcessFinished()) {
						addBackIntoQueue();
					}
					if(noAvailableProcess()) {
						noProcess();
					} else { 
						switchProcess();
					}
				}
			}
			
			timer++;
			drawNodes.get(drawNodes.size()-1).incrementEndTime();
			try {
				Thread.sleep(SPEED);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		
		calculateCPUUsage();
		calculateAverageWaitTime();
		calculateAverageTurnAroundTime();
		completed = true;
	}

	public void draw(Graphics g) {
		
		int x_mid;
		int x = 0;
		int width = 0;
		
		//Create Gantt Chart
		for(int i = 0; i < drawNodes.size(); i++) {
			x = SCALAR_X * (SHIFT_X + drawNodes.get(i).getStartTime());
			width = ((SCALAR_X * (SHIFT_X + drawNodes.get(i).getEndTime())) - x);
			
			g.setColor(drawNodes.get(i).getColor());
			g.fillRect(	x, 50, width, 40);
			g.setColor(Color.BLACK);
			g.drawRect( x, 50, width, 40);
		}
		x = 0;
		width = 0;
		//Create Gantt Chart
		for(int i = 0; i < drawNodes.size(); i++) {
			
			x = SCALAR_X * (SHIFT_X + drawNodes.get(i).getStartTime());
			width = ((SCALAR_X * (SHIFT_X + drawNodes.get(i).getEndTime())) - x);
			x_mid = x + (width / 2);
			
			//Draw Process Name
			g.setColor(Color.BLACK);
			g.drawString(drawNodes.get(i).getProcessName(), (x_mid - (drawNodes.get(i).getProcessName().length() * TEXT_SHIFT)), 30);

			//Draw Service Time
			g.setColor(Color.WHITE);
			g.drawString(String.valueOf(drawNodes.get(i).getServiceTime()), (x_mid - (String.valueOf(drawNodes.get(i).getServiceTime()).length() * TEXT_SHIFT)), 74);
			
			//Draw Times
			g.setColor(Color.BLACK);
			g.drawString(String.valueOf(drawNodes.get(i).getStartTime()), x, 110);
		}
	}
	
	public void addNewlyArrivingProcessesToQueue() {
		if(!processList.isEmpty()) {
			for(int i = 0; i < processList.size(); i++)
			{
				//if it has arrived, add it to queue
				if(processList.get(i).hasArrived(this.getTimer()))
					processQueue.add(processList.remove(i));
			}
		}
	}
	public String getFeedBackResults() {
		String results = feedback.toString();
		feedback = new StringBuffer();
		return results;
	}
	public int getTimer()
	{
		return timer;
	}
	public boolean noCurrentProcess() 
	{
		if(current_process == null)
			return true;
		else 
			return false;
	}
	public boolean noAvailableProcess()
	{
		if(processQueue.isEmpty())
			return true;
		else
			return false;
	}
	public boolean noFutureProcesses() 
	{
		if(processList.isEmpty())
			return true;
		else
			return false;
	}
	public void noProcess() {
		current_process = null;
		drawNodes.add(new DrawNode(timer));
	}
	
	public void switchProcess()
	{
		current_process = processQueue.poll();
		drawNodes.add(new DrawNode(	current_process.getProcessName(), 
				current_process.getColor(),
				timer,
				timer,current_process.getArrivalTime()));
	}
	public boolean currentProcessFinished()
	{
		if(current_process.getServiceTime() - drawNodes.get(drawNodes.size()-1).getServiceTime() <= 0)
			return true;
		else
			return false;
	}
	public int timeServicedOnCurrentProcess() {
		return drawNodes.get(drawNodes.size()-1).getServiceTime();
	}
	public int serviceTimeLeftOnCurrentProcess() {
		return current_process.getServiceTime() - timeServicedOnCurrentProcess();
	}
	
	public void addBackIntoQueue()
	{
		//Update Service Time to new time
		current_process.setServiceTime(serviceTimeLeftOnCurrentProcess());
		processQueue.add(current_process);
	}
	
	public void calculateCPUUsage() {
		double maxTime = drawNodes.get(drawNodes.size()-1).getEndTime() -1;
		double nonUsageTime = 0;
		for(int i = 0; i < drawNodes.size(); i++) {
			if(drawNodes.get(i).getProcessName().equals("NPA")) {
				nonUsageTime += drawNodes.get(i).getEndTime() - drawNodes.get(i).getStartTime()-1;
			}
		}
		double calculation = ((maxTime - nonUsageTime) / maxTime) * 100;
		DecimalFormat df = new DecimalFormat("###.#");
		feedback.append("\n\nCPU USAGE: " + df.format(calculation) +"%");
	}
	
	public void calculateAverageWaitTime() {
		double waitTime = 0;
		for(int i = 0; i < drawNodes.size(); i++) {
			if(drawNodes.get(i).getProcessName().equals("NPA")) {
				waitTime += drawNodes.get(i).getStartTime();
			}
		}
		
		double calculation = waitTime / num_processes;
		feedback.append("\nAverage Wait Time: " + calculation );
	}
	
	public void calculateAverageTurnAroundTime() {
		double turnAround = 0;
		int[] processes_start = new int[num_processes];
		Arrays.fill(processes_start, -1);
		int[] processes_end = new int[num_processes];
		Arrays.fill(processes_end, -1);
		
		for(int i = 0; i < drawNodes.size(); i++) {
			for(int j = 0; j < processes_start.length; j++) {
				if(("P"+(j+1)).equals(drawNodes.get(i).getProcessName()))
						if(processes_start[j] < 0)
							processes_start[j] = drawNodes.get(i).getArrivalTime();
				if(("P"+(j+1)).equals(drawNodes.get(i).getProcessName()))
					if(processes_end[j] < 0)
						processes_end[j] = drawNodes.get(i).getEndTime()-1;
			}
		}
		
		
		for(int i = 0; i < processes_start.length; i++){
			turnAround += processes_end[i] - processes_start[i];
		}
		
		double calculation = turnAround / num_processes;
		feedback.append("\nAverage Turn Around Time: " + calculation );	
	}
	public void setScalar(int scalar)
	{
		SCALAR_X = scalar;
	}
	public int getSimulationWidth()
	{
		return SCALAR_X * (SHIFT_X + timer);
	}
	public void stopThread()
	{
		newSimulationRequested = true;
	}
	
	public boolean isComplete() {
		return completed;
	}
}
